﻿/**
 * 网站前端监控系统
 * 
 * @version: 1.0.0
 * @author: hww
 * @date: 2025/02/19
 * */
; (function (root, factory) {
    if ('object' === typeof module && 'object' === typeof exports) {
        module.exports = factory();
    } else if ('function' == typeof define) {
        define('FrontendMonitoring', [], factory);
    } else {
        root.FrontendMonitoring = factory();
    }
})(window, function () {

    // 默认配置
    var defaults = {
        appId: 0,                                           // 应用Id
        cookieName_Uid: '',                                 // 获取指定 Cookie 值用来作为用户唯一Id的区分
        uploadUrl: '',                                      // 日志上传接收地址

        isUse: true,                                        // 是否启用监控
        isReported: true,                                   // 是否上报监控数据
        isLocalSaveLogs: true,                              // 是否本地存储监控日志
        localLogsName_Error: 'FrontendMonitoring_error_logs',    // 本地存储键名[异常监控]
        localSaveExpires: 7,                                // 本地存储有效期，单位：天。[默认7天]

        //------------- 以下配置默认通过后端直接获取，这里不传，减少需要发送的数据量

        cookieNamePrefix: '',                               // 要读取的 cookie 名称前缀
        excludeCookieNameArr: [''],                         // 排除读取的 cookie 名称数组
        isUploadUA: false                                   // 是否上传用户代理请求头
    };

    var Utils = {
        // 初始化配置
        initSetting: function (options) {
            if (options) {
                for (var key in defaults) {
                    var item = options[key];
                    if (typeof item !== 'undefined') {
                        defaults[key] = item;
                    }
                }
            }
        },

        // 获取用户设备信息
        userSystem: function () {
            var result = {};

            var userAgent = navigator.userAgent;

            //----------- 当前页面地址
            result.location = location.href;

            //----------- 使用设备
            var device_ipod = userAgent.match(/(ipod).*\s([\d_]+)/i),
                device_ipad = userAgent.match(/(ipad).*\s([\d_]+)/i),
                device_iphone = userAgent.match(/(iphone)\sos\s([\d_]+)/i),
                device_android = userAgent.match(/(android)\s([\d\.]+)/i),
                device_windows = userAgent.match(/(Windows).*\s([\d_]+)/i);

            var device = 'Unknown';

            if (device_android) {
                device = 'Android ' + device_android[2]
            } else if (device_iphone) {
                device = 'iPhone, iOS ' + device_iphone[2].replace(/_/g, '.')
            } else if (device_ipad) {
                device = 'iPad, iOS ' + device_ipad[2].replace(/_/g, '.')
            } else if (device_ipod) {
                device = 'iPod, iOS ' + device_ipod[2].replace(/_/g, '.')
            } else if (device_windows) {
                device = 'Windows ' + device_windows[2].replace(/_/g, '.')
            }

            var result_device = device,
                device_wechat = userAgent.match(/MicroMessenger\/([\d\.]+)/i);

            result_device += device_wechat && device_wechat[1] ? ', WeChat ' + device_wechat[1] : '';

            result.clientSystem = result_device;

            //----------- 所处网络环境
            var nettypeInfo = userAgent.toLocaleLowerCase().match(/ nettype\/([^ ]+)/g),
                result_nettype = 'Unknown';

            if (nettypeInfo && nettypeInfo[0]) {
                result_nettype = nettypeInfo[0].split('/')[1];

                result.networkType = result_nettype;
            }

            //----------- 用户代理信息
            result.UA = userAgent;

            return result;
        },

        /**
         * cookie 写入
         * 
         * @param {string} name cookie 名称
         * @param {object} value cookie 值
         * @param {number} expires 有效期，单位：天
         */
        writeCookie: function (name, value, expires) {
            expires = expires || defaults.localSaveExpires;
            var date = new Date();
            date.setTime(date.getTime() + expires * 24 * 60 * 60 * 1000);

            document.cookie = name + '=' + escape(value) + '; expires=' + date.toGMTString() + '; path=/weixin';
        },

        /**
         * cookie 读取
         * 
         * @param {string} name cookie 名称
         */
        readCookie: function (name) {
            var result = '';

            var cookie_arr = document.cookie.split('; ');
            for (var i = 0; i < cookie_arr.length; i++) {
                var item = cookie_arr[i].split('=');
                if (item[0] === name) {
                    result = unescape(item[1]);
                    break;
                }
            }
            return result;
        },

        /**
         * 根据 cookie 名称前缀获取相应 cookie 数组
         * 
         * @param {string} namePrefix cookie 名称前缀
         * @param {any} excludeNameArr 要排除的 cookie 名称数组
         * @returns 返回获取到的数组信息
         */
        readCookieArray: function (namePrefix, excludeNameArr) {
            var result = [];

            var cookie_arr = document.cookie.split('; ');
            for (var i = 0; i < cookie_arr.length; i++) {
                var item = cookie_arr[i].split('=');

                if (excludeNameArr && excludeNameArr.indexOf(item[0]) > -1) { continue; }

                if (namePrefix && item[0].indexOf(namePrefix) > -1) {
                    result.push({ name: item[0], value: unescape(item[1]) });
                }
            }

            return result;
        },

        /**
         * 写入本地存储
         * 
         * @param {string} key 本地存储名称
         * @param {string} value 要写入的数据
         * @returns 返回最终写入的数据
         */
        writeLocalStorage: function (key, value) {
            if (!window.localStorage) { return false }
            this.removeLocalStorage(key);

            var result = '';

            // 构建基础信息
            var systemInfo = Utils.userSystem(),
                date = new Date(),
                clientTime = date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate() + ' ' + date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds(),
                storageData = localStorage.getItem(key);

            value = escape(JSON.stringify(value));
            var cookieInfo = defaults.cookieNamePrefix && escape(JSON.stringify(Utils.readCookieArray(defaults.cookieNamePrefix, defaults.excludeCookieNameArr)));

            var info = {
                url: systemInfo.location,
                clientTime: clientTime,
                clientSystem: systemInfo.clientSystem,
                networkType: systemInfo.networkType,

                data: value
            };
            if (defaults.appId) { info.appId = defaults.appId; }
            if (defaults.cookieName_Uid) {
                var uidInfo = Utils.readCookie(defaults.cookieName_Uid);
                if (uidInfo) {
                    info.uId = uidInfo;
                }
            }
            if (cookieInfo) { info.cookieInfo = cookieInfo; }
            if (defaults.isUploadUA) { info.ua = systemInfo.UA; }

            info = JSON.stringify(info);


            if (storageData != null) {
                var storageDataArray = storageData.toString().split('|');

                // 已有相应的数据，则跳过本次写入
                if (storageDataArray.toString().indexOf(value) === -1) {
                    storageDataArray.push(info);
                    result = info;
                    localStorage.setItem(key, storageDataArray.join('|'));
                }
            } else {
                localStorage.setItem(key, info);
                result = info;
            }

            if (result) {
                result = JSON.parse(result);
                result.data = JSON.parse(unescape(result.data));
                if (result.cookieInfo) {
                    result.cookieInfo = JSON.parse(unescape(result.cookieInfo));
                }
            }

            return result;
        },

        /**
         * 移除已过期本地存储
         * 
         * @param {string} key 本地存储名称
         */
        removeLocalStorage: function (key) {
            if (!window.localStorage) { return false; }

            var storageData = localStorage.getItem(key);

            if (storageData != null) {
                var storageDataArray = storageData.toString().split('|');

                // 重新排序（按创建时间升序，获得最小的数据）
                storageDataArray.sort(function (a, b) {
                    var date_a = new Date(JSON.parse(a).clientTime.replace(/T/g, ' ').replace(/-/g, '/')),
                        date_b = new Date(JSON.parse(b).clientTime.replace(/T/g, ' ').replace(/-/g, '/'));

                    return date_a - date_b;
                });

                if (storageDataArray.length > 0) {
                    var firstCreateTime = JSON.parse(storageDataArray[0]).clientTime.replace(/T/g, ' ').replace(/-/g, '/'),
                        startTime = new Date(firstCreateTime).getTime(),
                        endTime = new Date().getTime(),
                        intervalDates = parseInt(Math.abs((startTime - endTime)) / (1000 * 60 * 60 * 24));  // 时间间隔（天）

                    if (intervalDates >= defaults.localSaveExpires) {
                        localStorage.removeItem(key);
                    }
                }
            }
        },

        /**
         * 读取本地存储
         * 
         * @param {string} key 本地存储名称
         */
        readLocalStorage: function (key) {
            if (!window.localStorage) { return false; }

            Utils.removeLocalStorage(key);

            var storageData = localStorage.getItem(key);
            if (storageData != null) {
                var storageDataArray = storageData.toString().split('|');

                // 重新排序（按创建时间降序）
                storageDataArray.sort(function (a, b) {
                    var date_a = new Date(JSON.parse(a).clientTime.replace(/T/g, ' ').replace(/-/g, '/')),
                        date_b = new Date(JSON.parse(b).clientTime.replace(/T/g, ' ').replace(/-/g, '/'));

                    return date_b - date_a;
                });

                var result = [];

                // 数据处理
                for (var key in storageDataArray) {
                    var item = JSON.parse(storageDataArray[key]);
                    item.data = JSON.parse(unescape(item.data));
                    if (item.cookieInfo) {
                        item.cookieInfo = JSON.parse(unescape(item.cookieInfo));
                    }
                    result.push(item);
                }

                return result;
            }

            return null;
        }
    };

    //---------- 系统构建
    function FrontendMonitoring(options) {
        var that = this;

        that.init(options);
    }

    FrontendMonitoring.prototype = {
        init: function (options) {
            var that = this;

            Utils.initSetting(options);
            if (!defaults.isUse) { return false; }
            that.errorHandle();
        },

        // 异常监控
        errorHandle: function () {
            var that = this;

            /**
             * 全局监听异常
             * @param {string} errorMessage 异常信息
             * @param {string} scriptURI    异常文件路径
             * @param {number} lineNo       异常行号
             * @param {number} columnNo     异常列号    
             * @param {any} error           异常堆栈信息
            */
            window.onerror = function (errorMessage, scriptURI, lineNo, columnNo, error) {
                var errorData = {
                    errorMessage: errorMessage || '',
                    scriptUrl: scriptURI || '',
                    lineNo: lineNo || 0,
                    columnNo: columnNo || 0,
                    stack: error && error.stack ? error.stack : null
                };

                that.dataUpload(1, errorData);
            }
        },

        // 性能监控
        performanceHandle: function () {

        },

        /**
         * 数据上报
         * 
         * @param {number} monitoringType 监控类型[1：异常监控、2：性能监控]
         * @param {any} data 要上报的数据
         */
        dataUpload: function (monitoringType, data) {
            var logsParams = '';

            if (defaults.isLocalSaveLogs) {
                if (monitoringType === 1) {
                    logsParams = Utils.writeLocalStorage(defaults.localLogsName_Error, data);
                }
            }

            if (defaults.isReported && defaults.uploadUrl && logsParams) {
                var img = new Image(1, 1);
                var src = defaults.uploadUrl + '?args=' + encodeURIComponent(JSON.stringify(logsParams));
                img.src = src;
            }
        }
    };

    return function (options) {
        return new FrontendMonitoring(options);
    }
});